/*
 * Copyright 2012 Andrea Parodi
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package eban.storebyrules;


import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.UnmodifiableIterator;
import eban.storebyrules.filters.AbstractFilter;
import eban.storebyrules.results.Result;
import eban.storebyrules.typemanagement.FieldTypeManager;
import sun.reflect.ReflectionFactory;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;


public final class PersistentCollectionImpl<TBean> implements PersistentCollection<TBean>{
    public static final String CANNOT_INSTANTIATE_NEW_BEAN_INSTANCE = "cannot instantiate new bean instance";
    public static final String OBJECT_LIVE_EXCEPTION = "Object of class %s \n with key %s is live and can not be deleted.\n Call detach() or delete() in order to remove it.";

    public Storage getStorage() {
        return storage;
    }

    private final Storage storage;

    public Class<TBean> getClazz() {
        return clazz;
    }

    public StorageLanguageBuilder getLanguageBuilder() {
        return languageBuilder;
    }

    private final Class<TBean> clazz;


    public PersistentCollectionImpl(Storage storage, Class<TBean> clazz) {
        Preconditions.checkNotNull(storage, "storage parameter");
        Preconditions.checkNotNull(clazz, "clazz parameter");
        this.storage = storage;
        this.clazz = clazz;
        this.runtimeConverter = new RuntimeConverter<TBean>(clazz);


    }


    public List<TBean> load() throws PersistenceException {
        return load(null,true);
    }

    public List<TBean> load(AbstractFilter filter) throws PersistenceException {
        return load(filter, true);
    }

    private List<TBean> load(AbstractFilter filter, boolean trackInstance) throws PersistenceException {
        List<TBean> values = Lists.newArrayList();


        Result rs = null;

        try {
            rs = storage.getRecords(languageBuilder.selectCommand(filter));
            for (Result.Row row : rs) {

                TBean instance = (TBean) runtimeConverter.buildNewObject(row);
                values.add(instance);
                if (trackInstance)
                    PersistedItemWeakList.getInstance().add(runtimeConverter.keyValue(instance), instance);
            }
            return values;
        } catch (Exception e) {
            throw new PersistenceException("Unable to load data.", e);
        } finally {
            if (rs!=null)
                rs.close();
        }


    }


    private final RuntimeConverter runtimeConverter;


    protected String keyName() {
        return runtimeConverter.keyName();

    }


    protected String keyValue(TBean instance) {
        return runtimeConverter.engineKeyValue(instance);
    }


    public void delete(Transaction transaction, TBean instance) throws PersistenceException {


        transaction.execute(languageBuilder.deleteCommand(instance));

    }


    private final StorageLanguageBuilder languageBuilder = new StorageLanguageBuilder(this);


    /**
     * This
     *
     * @param transaction
     * @param instance
     * @throws PersistenceException
     */
    public void store(Transaction transaction, TBean instance) throws PersistenceException {

        if (isPersisted(instance)) {
            update(transaction, instance);
        } else {
            insert(transaction, instance);
            PersistedItemWeakList.getInstance().add(runtimeConverter.keyValue(instance), instance);
        }

    }

    public void detach(TBean instance){
        PersistedItemWeakList.getInstance().detach(runtimeConverter.keyValue(instance),instance.getClass());
    }

    public void clear(Transaction transaction) throws PersistenceException {
        for (TBean instance : load(null,false)) {
            PersistedItemWeakList weakList = PersistedItemWeakList.getInstance();
            Object key = runtimeConverter.keyValue(instance);
            if (weakList.isPersistent(key, instance.getClass())) {
                throw new PersistenceException(String.format(OBJECT_LIVE_EXCEPTION, instance.getClass(), key));
            }
        }
        transaction.execute("DELETE FROM "+languageBuilder.getTableName(clazz));
    }

    private boolean isPersisted(TBean instance) {
        return PersistedItemWeakList.getInstance().isPersistent(runtimeConverter.keyValue(instance), instance.getClass());
        //return !instance.isNewItem();
    }

    private void insert(Transaction transaction, TBean instance) throws PersistenceException {


        transaction.execute(languageBuilder.insertCommand(instance));

        if (runtimeConverter.keyAnnotation().auto())
            retrieveAutoKey(instance);


    }


    private void retrieveAutoKey(TBean instance) throws PersistenceException {
        try {
            String sql = languageBuilder.retrieveAutoKeyCommand();
            Result recordset = storage.getRecords(sql);

            runtimeConverter.setFieldValueFromRow(
                    instance,
                    recordset.current(),
                    recordset.columns().byName(runtimeConverter.keyName())
            );
            recordset.close();
        } catch (SecurityException e) {

            throw new PersistenceException("Unable to update data.", e);

        } catch (SQLException e) {

            throw new PersistenceException("Unable to update data.", e);
        }
    }


    public String createTableSql() {


        try {
            ReflectionFactory rf = ReflectionFactory.getReflectionFactory();
            Constructor objDef = clazz.getSuperclass().getDeclaredConstructor();
            Constructor intConstr = rf.newConstructorForSerialization(clazz, objDef);
            TBean instance = clazz.cast(intConstr.newInstance());
            return languageBuilder.createTableCommand(instance);


        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new IllegalStateException("Cannot create object", e);
        }


    }


    private void update(Transaction transaction, TBean instance) throws PersistenceException {
        Preconditions.checkNotNull(instance);
        Object keyValue = null;
        try {
            keyValue = keyValue(instance);
            Preconditions.checkNotNull(keyValue);
        } catch (IllegalArgumentException e1) {

            throw new PersistenceException("Unable to update data.", e1);
        }


        String sql = languageBuilder.updateCommand(instance, keyValue);


        transaction.execute(sql);

    }

    private static class ManagerForType {
        private final Class<?> typeOfField;
        private FieldTypeManager<?> typeManager;

        public void setTypeManager(FieldTypeManager<?> typeManager) {
            this.typeManager = typeManager;
        }

        public Class getTypeOfField() {
            return typeOfField;
        }

        public FieldTypeManager getTypeManager() {
            return typeManager;
        }

        private ManagerForType(Class<?> typeOfField, FieldTypeManager<?> typeManager) {

            this.typeOfField = typeOfField;
            this.typeManager = typeManager;
        }
    }

    private Map<Field, ManagerForType> cachedFieldMap;

    public Map<String, String> getFieldsMap(TBean instance) throws PersistenceException {
        Preconditions.checkNotNull(instance);
        Preconditions.checkArgument(instance.getClass().equals(clazz), "instance must be of class " + clazz.getName());

        if (cachedFieldMap == null) {
            cachedFieldMap = Maps.newHashMap();
            UnmodifiableIterator iterator = runtimeConverter.persistentFields().iterator();
            for (Field m = (Field) iterator.next(); iterator.hasNext(); m = (Field) iterator.next()) {
                if (!runtimeConverter.isKey(m) || !runtimeConverter.keyAnnotation().auto()) {
                    Class<?> typeOfField = runtimeConverter.getTypeOfField(instance, m.getName());
                    FieldTypeManager typeManager = FieldTypeManager.Registry.findManager(typeOfField);
                    cachedFieldMap.put(m, new ManagerForType(typeOfField, typeManager));
                }
            }
        }
        Map<String, String> fields = Maps.newHashMap();
        for (Map.Entry<Field, ManagerForType> fieldEntry : cachedFieldMap.entrySet()) {
            String sqlValue = null;
            Field field = fieldEntry.getKey();
            try {

                Object value = field.get(instance);
                Class<?> typeOfField = runtimeConverter.getTypeOfField(instance, field.getName());
                ManagerForType managerForType = fieldEntry.getValue();
                if (!managerForType.getTypeOfField().equals(typeOfField))
                    managerForType.setTypeManager(FieldTypeManager.Registry.findManager(typeOfField));

                sqlValue = managerForType.getTypeManager().toEngineValue(value);
                fields.put(field.getName(), sqlValue);
            } catch (Exception e) {
                throw new RuntimeException("Unable to set field " + field.getName(), e);
            }
        }
        return fields;

    }


    public ImmutableCollection<Field> persistentFields() {
        return runtimeConverter.persistentFields();
    }
}
